<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>createWebHistory</title>
</head>
<body>
<!--
  前端路由的实现原理, 两种模式, 一种是hash模式, 一种是history模式
  windows.location.hash = '/'   history.pushState(state,null,url)   history.replaceState()
  目前浏览器都支持了history.pushState (不考虑ie), 故直接使用history.pushState来实现两种模式即可
  + history.pushState(state,null,url)
  + history.pushState(state,null,'#/')

  两种路由的区别:
  + hash:
    - hash模式的好处 就是锚点, url栏回车的时候也不会向服务器发请求 （只会重新进行页面定位
    - 缺点是： 不支持服务端渲染 (因为不会发出请求), 不会产生404
  + history:
    - 特点就是路径漂亮, 没有#
    - url栏回车的时候, 会向服务器发送请求, 如果资源不存在会出现404
      + 解决方案是服务端统一返回渲染首页, 首页会根据路径重新跳转
      + 或则如果支持服务端渲染, 就是直接返回对应渲染后的页面
-->


<button onclick="history.push('/')">首页</button>
<button onclick="history.push('/about')">关于我</button>
<button onclick="history.replace('/xxx')">替换</button>

<script>
  // 路由系统最基本的得包含
  // 当前的路径、当前路径下他的状态是什么
  // 需要提供两个切换路径的方法 push replace
  // 路由的监听, 如果路径变化, 需要通知用户
  const history = /*createWebHistory()*/createWebHashHistory()
  history.listen((to, from, {isBack}) => {
    console.log(to, from, {isBack});
  });

  function createWebHashHistory() {
    return createWebHistory('#');
  }

  function createWebHistory(base = '') {
    const historyNavigation = useHistoryStateNavigation(base);

    const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location);

    const routerHistory = Object.assign({}, historyNavigation, historyListeners);

    Object.defineProperty(routerHistory, 'location', {
      get: () => historyNavigation.location.value
    });
    Object.defineProperty(routerHistory, 'state', {
      get: () => historyNavigation.state.value
    })

    return routerHistory;
  }

  function useHistoryListeners(base, historyState, currentLocation) {
    const listeners = [];

    const popStateHandler = ({state}) => { //最新的状态, 已经前进后退完毕了
      console.log('state:', state);
      const to = createCurrentLocation(base);
      const from = currentLocation.value; //当前的 上一次的location （还没更新成现在的）
      const fromState = historyState.value; //当前的 上一次的state （还没更新成现在的）

      currentLocation.value = to;
      historyState.value = state; //todo state 可能会为null

      const isBack = state.position - fromState.position < 0;

      //执行用户扩展
      listeners.forEach(l => {
        l(to, from, {isBack});
      });
    };

    window.addEventListener('popstate', popStateHandler) //pushState是不会触发的, 只有浏览器的前进后退, 调用历史栈改变的

    function listen(cb) {
      listeners.push(cb);
    }

    return {
      listen //用户烤靠此进行扩展
    }
  }

  function useHistoryStateNavigation(base) {
    const currentLocation = {
      value: createCurrentLocation(base) //因为后面可能会接入vue 要响应式变化 故需要 . 才能被收集依赖
    };
    const historyState = {
      value: window.history.state
    }

    /** 初始化路由状态 */
    // 第一次刷新页面,此时没有任何状态 那么我就自己维护一个状态
    // 这个状态是我们自己去写的, 里面可以包含我自己的一些信息
    // 比如页面后退后是哪个路径, 当前路径是哪个, 要去哪里, 我是用的push跳转还是replace跳转, 跳转后滚动条位置是哪
    if (!historyState.value) {
      changeLocation(currentLocation.value, buildState(null, currentLocation.value, null, true), true);
    }

    function changeLocation(to, state, replace) {
      const hasPos = base.indexOf('#');
      const url = hasPos > -1 ? base + to : to;
      window.history[replace ? 'replaceState' : 'pushState'](state, null, url);
      historyState.value = state;
    }

    function push(to, data) {
      // 跳转的时候 需要做两个状态
      // 一个是 跳转前： 从哪去哪
      // 一个是 跳转后: 从这到了那
      // 主要是为了方便插入路由钩子 beforeXxx

      const currentState = Object.assign(
        {},
        historyState.value, // 当前的状态
        {
          forward: to,
          scroll: {left: window.pageXOffset, top: window.pageYOffset}
        }
      );
      // 本质是没有跳转的 只是更新了状态, 后续在vue中可以详细的监控到状态的变化
      changeLocation(currentState.current, currentState, true); //这没跳, 只是改了状态

      //todo 这里可以插入钩子

      const state = Object.assign(
        {},
        buildState(currentLocation.value, to, null),
        {position: currentState.position + 1},
        data
      );
      //这里才真正跳了
      changeLocation(to, state, false);
      currentLocation.value = to;
    }

    function replace(to, data) {
      const state = Object.assign(
        {},
        buildState(historyState.value.back, to, historyState.value.forward, true),
        data
      );
      changeLocation(to, state, true);
      currentLocation.value = to; //替换后需要将路径变为现在的路径
    }

    return {
      location: currentLocation,
      state: historyState,
      push,
      replace
    }
  }

  function buildState(back, current, forward, replace = false, computedScroll = false) {
    console.log('replace:', replace);
    return {
      back,
      current,
      forward,
      replace,
      scroll: computedScroll ? {left: window.pageXOffset, top: window.pageYOffset} : null,
      position: window.history.length - 1
    }
  }

  function createCurrentLocation(base) {
    const {pathname, search, hash} = window.location;

    const hasPos = base.indexOf('#'); // #/about
    if (hasPos > -1) {
      return base.slice(1) || '/';
    }

    return pathname + search + hash;
  }
</script>
</body>
</html>
